<?php

/**
 * Расширение Zend_Cache_Backend_Memcached.
 * В расширении доступна эмуляция тегов.
 * Схему реализации можно почитать тут:
 * http://habrahabr.ru/blogs/webdev/43539/
 * 
 * @todo
 * Кроме этого тут должны будут появиться методы, предотвращающие 
 * одновременное перестроение кешей. Подробности тут:
 * http://habrahabr.ru/blogs/webdev/43540/
 * 
 * Эти методы сейчас в слотах и они неочень.
 */
class Cache_Backend_Memcached extends Zend_Cache_Backend_Memcached 
{
	/**
	 * Возвращает исходный класс Memcache на котором базируется всё кэширование.
	 * 
	 * @return Memcache
	 */
	public function getRawClass()
	{
		return $this->_memcache;
	}
	
	/**
	 * Добавляет тег к значению, записываемому в memcache, 
	 * а также добавляем тег с новой версией в сам memcache, если его не существует.
	 * 
	 * @param string $tag
	 * @param array $value Передается по ссылке
	 * @return void
	 */
	protected function _addTag($tag, &$value)
	{
	    if ($this->_options['compression']) {
            $flag = MEMCACHE_COMPRESSED;
        } else {
            $flag = 0;
        }
        
		$key = 'memcachetag_' . $tag;
		$version = $this->_memcache->get($key);
		if (!$version) {
			$version = time();
			$this->_memcache->set($key, $version, $flag, $this->getLifetime(false));
		}
		
		$value['tag'][$tag] = $version;
	}
	
    /**
     * Save some string datas into a cache record
     *
     * Note : $data is always "string" (serialization is done by the
     * core not by the backend)
     *
     * @param  string $data             Datas to cache
     * @param  string $id               Cache id
     * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
     * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
     * @return boolean True if no problem
     */
    public function save($data, $id, $tags = array(), $specificLifetime = false)
    {
    	$t = microtime(true);
        $lifetime = $this->getLifetime($specificLifetime);
        if ($this->_options['compression']) {
            $flag = MEMCACHE_COMPRESSED;
        } else {
            $flag = 0;
        }

        $value = array('value' => $data, 'created' => time(), 'lifetime' => $lifetime);
        foreach ($tags as $tag) {
        	$this->_addTag($tag, $value);
        }
        
        // ZF-8856: using set because add needs a second request if item already exists
        $result = $this->_memcache->set($id, $value, $flag, $lifetime);
        if (Debug::getInstance()->isDebugMode()) {
        	$query = "set $id";
        	$waste = microtime(true) - $t;
        	Cache_Profile::getInstance()->addQuery($query, $waste);
        }
        
        return $result;
    }
    
    /**
     * Добавлено профилирование. Работает только в отладочном режиме.
     * Test if a cache is available for the given id and (if yes) return it (false else)
     *
     * @param  string  $id                     Cache id
     * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
     * @return string|false cached datas
     */
    public function load($id, $doNotTestCacheValidity = false)
    {
    	$t = microtime(true);
        $result = $this->_memcache->get($id);
        
        // Если в записи есть теги - проверяем их версии
        if (isset($result['tag'])) {
        	foreach ($result['tag'] as $tag => $version) {
        		$key = 'memcachetag_' . $tag;
        		$v = $this->_memcache->get($key);
        		if ($v != $version) {
        			return false;
        		}
        	}
        }
        
    	if (Debug::getInstance()->isDebugMode()) {
    		$textResult = (!is_array($result)) ? ' MISS' : '';
        	$query = "get {$id}{$textResult}" ;
        	$waste = microtime(true) - $t;
        	Cache_Profile::getInstance()->addQuery($query, $waste);
    	}
    	
    	if (isset($result['value'])) return $result['value'];
    	return false;
    }
    
    /**
     * Remove a cache record
     *
     * @param  string $id Cache id
     * @return boolean True if no problem
     */
    public function remove($id)
    {
        return $this->_memcache->delete($id, 0);
    }
	
    /**
     * Clean some cache records.
     * В данном методе указана только очистка данных по тегам.
     *
     * Available modes are :
     * 'all' (default)  => remove all cache entries ($tags is not used)
     * 'old'            => unsupported
     * 'matchingTag'    => remove all cache entries by tag
     * 'notMatchingTag' => unsupported
     * 'matchingAnyTag' => unsupported
     *
     * @param  string $mode Clean mode
     * @param  array  $tags Array of tags
     * @throws Zend_Cache_Exception
     * @return boolean True if no problem
     */
    public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
    {
    	if ($mode != Zend_Cache::CLEANING_MODE_MATCHING_TAG) {
    		parent::clean($mode, $tags);
    		return;
    	}

    	if ($this->_options['compression']) {
            $flag = MEMCACHE_COMPRESSED;
        } else {
            $flag = 0;
        }
        
    	foreach ($tags as $tag) {
    		$key = 'memcachetag_' . $tag;
    		$this->_memcache->set($key, time(), $flag, $this->getLifetime(false));
    	}
    }
	
    /**
     * Return an array of stored tags
     *
     * @return array array of stored tags (string)
     */
    public function getTags()
    {
    	throw new Cache_Exception(__CLASS__ . '::' . __METHOD__ . ' is not implemented');
    }

    /**
     * Return an array of stored cache ids which match given tags
     *
     * In case of multiple tags, a logical AND is made between tags
     *
     * @param array $tags array of tags
     * @return array array of matching cache ids (string)
     */
    public function getIdsMatchingTags($tags = array())
    {
        throw new Cache_Exception(__CLASS__ . '::' . __METHOD__ . ' is not implemented');
    }

    /**
     * Return an array of stored cache ids which don't match given tags
     *
     * In case of multiple tags, a logical OR is made between tags
     *
     * @param array $tags array of tags
     * @return array array of not matching cache ids (string)
     */
    public function getIdsNotMatchingTags($tags = array())
    {
        throw new Cache_Exception(__CLASS__ . '::' . __METHOD__ . ' is not implemented');
    }

    /**
     * Return an array of stored cache ids which match any given tags
     *
     * In case of multiple tags, a logical AND is made between tags
     *
     * @param array $tags array of tags
     * @return array array of any matching cache ids (string)
     */
    public function getIdsMatchingAnyTags($tags = array())
    {
        throw new Cache_Exception(__CLASS__ . '::' . __METHOD__ . ' is not implemented');
    }
	
}